/*
 * TCSS 558
 * Distributed HashMap
 * Fall 2013
 * James Marquardt
 * Matt Adams
 * Justin Wong
 */
package model;
import static com.google.common.base.Preconditions.*;
import tools.CheckArgument;
import tools.Log;
import node.Node;
/**
 * Table used by a node to store references to other nodes in a chord.
 * 
 * @author Matt Adams
 */
public class FingerTable {

    /**
     * Finger table data.
     */
    private final Node[] fingerNodes;

    /**
     * Node references for this local node.
     */
    private final NodeReferences references;
    
    /**
     * The local node ID.
     */
    private final int localID;
    
    /**
     * Constructs FingerTable object.
     * 
     * @param localID The local node ID.
     * @param references NodeReferences object.
     */
    public FingerTable(final int localID, final NodeReferences references) {
        this.localID = localID;
        this.references = references;
        this.fingerNodes = new Node[references.getMValue()];
    }
    
    /**
     * Sets a given index of the finger table to the given reference.
     * 
     * @param index Index of table entry.
     * @param nodeReference Reference to store.
     */
    public final void setFinger(int index, Node nodeReference) {
        checkArgument(index >= 0 || index < this.fingerNodes.length, "Index must be greater than 0 and less than " + this.fingerNodes.length);
        checkNotNull(nodeReference, "Node reference cannot be null.");

        this.fingerNodes[index] = nodeReference;
        //Need log
    }
    
    /**
     * Returns the reference stored at the given index.
     * 
     * @param index Index of finger to be returned.
     * @return Reference stored at the given index.
     */
    public final Node getFinger(int index) {
        checkArgument(index >= 0 || index < this.fingerNodes.length, "Index must be greater than 0 and less than " + this.fingerNodes.length);

        return fingerNodes[index];
    }
    
    /**
     * Adds a nodeReference to all appropriate finger table indexes. It will be placed into the finger table
     * if it satisfies that the start index is in the interval (local node ID, new node ID). The currently 
     * stored entry must be null or the new node ID must be in the interval (local node ID, currently stored
     * node ID). 
     * 
     * @param nodeReference Node reference being added to the finger table.
     */
    public final void addReference(Node nodeReference) {
        checkNotNull(nodeReference, "Node reference added to finger table cannot be null.");
        
        final int nodeReferenceID = nodeReference.getNodeID();
        Log.out("Finger Table Update. Attempting to add reference node: " + nodeReferenceID + " To node: " + this.localID);
        for (int i = 0; i < this.fingerNodes.length; i++) {
            
            final int startOfInterval = (this.localID + (int) Math.pow(2, i)) % (int) Math.pow(2, this.fingerNodes.length);
            if (!CheckArgument.isInInterval(startOfInterval, this.localID, nodeReferenceID)) {
                //Start index is NOT in interval (local node ID, new node ID).
                Log.out("No reference added. Not contained in interval: check(" + startOfInterval + ") interval(" + this.localID + ", " + nodeReferenceID + ")");
                break;
            }
            if (getFinger(i) == null) {
                Log.out("Succesfully added reference to finger table (current entry was null) at index: " + i);
                setFinger(i, nodeReference);
            } else if (CheckArgument.isInInterval(nodeReferenceID, this.localID, getFinger(i).getNodeID()) && nodeReferenceID != getFinger(i).getNodeID()) {
                //New node ID IS in the interval (local node ID, currently stored node ID).
                setFinger(i, nodeReference);    
                Log.out("Successfully added reference to finger table at index: " + i + ". Replaced old entry with ID: " + getFinger(i).getNodeID());
                //Node oldEntry = getFinger(i);
           
                //this.references.disconnectNodeIfUnused(oldEntry);
            }
        }
    }
    
    /**
     * Determines closest preceding node of given ID.
     * 
     * @param key ID of which the closest preceding node shall be determined.
     * @return Reference to the node which most closely precedes the given ID.
     *         null if no node has been found.
     */
    public final Node getClosestPrecedingNode(final int key) {
        checkArgument(key >= 0 || key < Math.pow(2, this.fingerNodes.length) - 1, 
                "Index must be greater than 0 and less than " + ((int) Math.pow(2, this.fingerNodes.length) - 1));
        Node result = null;
        for (int i = this.fingerNodes.length - 1; i >= 0; i--) {
            if (this.fingerNodes[i] != null && 
                    CheckArgument.isInInterval(this.fingerNodes[i].getNodeID(), this.localID, key)) {
                
                result = this.fingerNodes[i];
                break;
            }
        }
        //No closest preceding node for ID
        return result;
    }
    
    /**
     * Removes all occurences of the given node from finger table.
     * 
     * @param removeNode Reference to be removed from the finger table.
     */
    public final void removeReference(Node removeNode) {
        checkNotNull(removeNode, "Reference to be removed cannot be null.");

        //Find node reference to replace the reference to be removed.
        Node replaceReference = null;
        for (int i = this.fingerNodes.length - 1; i >= 0; i--) {
            Node fingerNode = this.getFinger(i);
            if (removeNode.equals(fingerNode)) {
                break;
            }
            if (fingerNode != null) {
                replaceReference = fingerNode;
            }
        }
        //Remove all occurrences of reference
        for (int i = 0; i < this.fingerNodes.length; i++) {
            if (removeNode.equals(this.fingerNodes[i])) {
                if (replaceReference == null) {
                    unsetFinger(i);
                } else {
                    setFinger(i, replaceReference);
                }
            }
        }
        if (this.references.getSuccessor().equals(removeNode)) {
            this.references.setSuccessor(null);
        }
        //Attempt to add reference of successor to fill null spaces in table.
        if (this.references.getSuccessor() != null) {
            this.addReference(this.references.getSuccessor());
        }

    }
    
    public void unsetFinger(final int index) {
        // remember overwritten reference
        Node replacedNode = this.getFinger(index);
        // set reference to null
        this.fingerNodes[index] = null;

        if (replacedNode != null) {
            // check if overwritten reference does not exist any more
            //this.references.disconnectNodeIfUnused(overwrittenNode);
        }
    }
    
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append("Finger Table for node ID: " + this.localID);
        sb.append("\nIndex\tStart\tSuccessor\n");
        for (int i = 0; i < this.fingerNodes.length; i++) {
            final int startOfInterval = (this.localID + (int) Math.pow(2, i)) % (int) Math.pow(2, this.fingerNodes.length);
            sb.append(i);
            sb.append("\t");
            sb.append(startOfInterval);
            sb.append("\t");
            if (getFinger(i) == null) {
                sb.append("null");
            } else {
                sb.append(getFinger(i).getNodeID());
            }
            sb.append("\n");
        }       
        
        return sb.toString();
    }
    
    /**
     * Determines if the given reference is stored somewhere in the finger
     * table.
     * 
     * @param newReference Reference being checked.
     * @return true, if the given reference exists in the finger
     *         table, or else false.
     */
    public final boolean containsReference(Node newReference) {
        boolean result = false;
        checkNotNull(newReference, "Reference to check cannot be null.");
        for (int i = 0; i < this.fingerNodes.length; i++) {
            if (newReference.equals(this.fingerNodes[i])) {
                result = true;
                break;
            }
        }
        return result;
    }
    
    public Node[] getFullFingerTable(){
    	return fingerNodes;
    }
}
